Skip to content

perf: fix build errors concurrently with bounded fan-out - #1336

Merged
groupthinking merged 4 commits into
mainfrom
perf/ai-fix-parallel
Aug 4, 2026
Merged

perf: fix build errors concurrently with bounded fan-out#1336
groupthinking merged 4 commits into
mainfrom
perf/ai-fix-parallel

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1335

Scope

Two files:

  • src/youtube_extension/backend/ai_code_generator.pyfix_build_errors only
  • tests/unit/test_ai_code_generator.py — new TestFixBuildErrorsConcurrency class

No call sites change. The public return shape ({"success", "fixed_files", "attempted"}) is untouched, and max_concurrency is a new optional keyword,
so every existing caller is source-compatible.

Outcome

fix_build_errors repaired each failing file in a strictly serial loop. For
each file it ran a blocking Path.exists() + Path.read_text(), awaited an LLM
round-trip, then ran a blocking Path.write_text() — all on the event loop
thread.

The per-file work is mutually independent: each file gets its own prompt, its
own completion and its own write, and no iteration consumes state produced by a
previous one. The serialisation was incidental, not required.

This PR:

  • offloads read_text / write_text with asyncio.to_thread, so the loop is
    no longer blocked on file syscalls;
  • fans the per-file work out with asyncio.gather, bounded by an
    asyncio.Semaphore;
  • defaults that bound to _MAX_CONCURRENT_FIXES = 4 and exposes an optional
    max_concurrency override;
  • sorts fixed_files, which was previously in nondeterministic order because
    error_files is a set.

With an LLM round-trip dominating each iteration, N broken files now cost
ceil(N / 4) sequential batches instead of N.

Risk

Low. Deliberately bounded rather than unbounded.

  • Provider rate limits. Unbounded gather over a large error list would fire
    every request at once. The semaphore caps in-flight requests at 4 by default.
    A dedicated test asserts the default bound is never exceeded.
  • Total requests are unchanged. This is a scheduling change, not an
    amplification. Exactly one generate call per error file is issued, before
    and after — so there is no change in billed LLM spend.
  • Failure isolation. The original loop used continue to skip a bad file. The
    helper now returns None for the same cases and gather runs with the
    default return_exceptions=False, which is safe precisely because the helper
    never propagates: missing files, OSError on read, and provider exceptions
    are all caught and converted to None. Two tests cover this.
  • Cancellation. No finally block in this path touches shared state, so the
    shield/drain pattern needed in perf: offload blocking SQLite I/O off the event loop #1327 does not apply here; a plain
    to_thread is correct.
  • Result ordering. Sorting is a behaviour change, but a strictly stabilising
    one — the previous order was set-iteration order. No existing test asserted on
    it (verified across all 10 tests in TestFixBuildErrors).

Verification

Prove-fail. The new class was run against the unmodified source via
git stash push -- src/youtube_extension/backend/ai_code_generator.py:

7 failed, 2 passed, 241 deselected

The 7 failures are the new-behaviour assertions:

FAILED test_files_are_fixed_concurrently
FAILED test_default_concurrency_is_bounded
FAILED test_max_concurrency_override_is_respected
FAILED test_non_positive_max_concurrency_clamps_to_one[0]
FAILED test_non_positive_max_concurrency_clamps_to_one[-5]
FAILED test_fixed_files_order_is_deterministic
FAILED test_file_io_runs_off_the_event_loop_thread

The 2 that passed pre-change are the failure-isolation cases — they are
regression guards for semantics this PR must preserve, so passing on both
sides is the correct result.

After restoring the change:

.venv/bin/python -m pytest tests/unit/test_ai_code_generator.py \
  tests/unit/test_deployment_manager.py --override-ini="addopts=" -p no:cacheprovider -q
361 passed in 4.22s

That is the pre-existing 352 plus the 9 added here — no regressions in the 10
existing TestFixBuildErrors tests, nor in test_deployment_manager.py, which
mocks fix_build_errors at three call sites.

How the concurrency assertions work: router.generate is replaced with a stub
that increments a counter, await asyncio.sleep(0.05), then decrements —
recording the peak. A serial loop can never record a peak above 1, so
max_inflight > 1 is only satisfiable by genuine concurrency. Thread placement
is asserted by monkeypatching Path.read_text/write_text to record
threading.get_ident() and checking the loop's own ident appears in neither
list.

ruff check on both changed files reports 5 F841s, all pre-existing on
origin/main (lines 668/675/682/689/1988) and none inside the added class.

Production evidence

DeploymentManager.verify_and_fix_project
(src/youtube_extension/backend/deployment_manager.py:343) calls
fix_build_errors from inside a retry loop declared at line 301. The serial
cost was therefore multiplied by max_retries on every deployment whose build
fails — the exact path where latency is most visible to a waiting user.

The semaphore idiom used here matches the existing one in the same file at
line 621, so this introduces no new concurrency pattern to the codebase.

Agent handoff

Nothing outstanding. Behaviour is covered by 9 tests, 7 of which are
prove-failed against the pre-change source. If provider limits ever tighten,
_MAX_CONCURRENT_FIXES is a single module-level constant, and callers can
already pass max_concurrency per invocation without a code change here.

fix_build_errors repaired each failing file in a strictly serial loop,
performing blocking Path.read_text/write_text on the event loop thread
around each LLM round-trip. The files are mutually independent, so wall
clock scaled linearly with the number of broken files.

- offload read_text/write_text via asyncio.to_thread
- fan out per-file fixes with asyncio.gather, bounded by a semaphore
  (default 4, overridable via the new max_concurrency parameter)
- clamp non-positive max_concurrency to 1
- sort fixed_files for deterministic results (error_files is a set)

Failure semantics are unchanged: a missing, unreadable, or
provider-failed file is skipped without aborting its siblings.

DeploymentManager.verify_and_fix_project calls this from inside a retry
loop, so the serial cost was multiplied by max_retries per deployment.

Closes #1335

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 4, 2026 03:46
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Canceled Canceled Aug 4, 2026 4:10am

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@groupthinking, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54eb4cba-2d7d-4d1b-94e0-b9f34a748968

📥 Commits

Reviewing files that changed from the base of the PR and between 9a000d7 and af277dd.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_ai_code_generator.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/ai_code_generator.py
📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved build-error fixing reliability by handling missing files, read failures, empty responses, and unsuccessful fixes without interrupting other repairs.
    • Build fixes are now processed concurrently with controlled limits for better responsiveness.
    • Results are returned in a stable order for more predictable behavior.

Walkthrough

fix_build_errors now repairs files concurrently with a configurable limit of four by default. File reads and writes run off the event loop. Individual failures remain isolated, and successful file paths are returned in sorted order.

Changes

Build error fix processing

Layer / File(s) Summary
Concurrency configuration
src/youtube_extension/backend/ai_code_generator.py
fix_build_errors accepts optional max_concurrency and applies the module default of four.
Bounded parallel file repairs
src/youtube_extension/backend/ai_code_generator.py
File I/O runs through worker threads. Per-file repairs run under a semaphore, failures return None, and successful paths are gathered in deterministic order.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant fix_build_errors
  participant FileSystem
  participant Router
  fix_build_errors->>FileSystem: Read file via asyncio.to_thread
  fix_build_errors->>Router: Generate fix
  Router-->>fix_build_errors: Return generated response
  fix_build_errors->>FileSystem: Write fix via asyncio.to_thread
  fix_build_errors-->>fix_build_errors: Gather sorted successful paths
Loading

Possibly related PRs

Suggested labels: copilot-rabbit

Suggested reviewers: copilot, claude

Poem

Four fixes fly, then safely wait,
Threads read files without loop-state weight.
LLM replies guide each repair,
Sorted paths return with care.
One failure leaves the rest alive.

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR meets the performance objectives, but invalid text reads can abort sibling repairs and cancellation can leave a worker-thread write running. Catch non-cancellation read exceptions such as UnicodeDecodeError, and track, shield, and drain the write task before propagating cancellation.
Enforce Copilot Verification ⚠️ Warning GitHub reports copilot-pull-request-reviewer[bot] reviewed PR #1336 with state COMMENTED; no Copilot APPROVED review exists. Obtain an explicit APPROVED review from GitHub Copilot for the current PR revision. Resolve or address the Copilot comments before rechecking.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: concurrent build-error fixes with bounded concurrency.
Description check ✅ Passed The description covers the main template sections and provides detailed scope, risk, verification, and operational impact.
Out of Scope Changes check ✅ Passed The reviewed implementation changes only fix_build_errors and directly supports the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Require Ai Unit Tests ✅ Passed PR #1336 has the copilot-rabbit label; commit 9a000d7 includes 160 additions in tests/unit/test_ai_code_generator.py alongside the implementation and is co-authored by Copilot App.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/ai-fix-parallel
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch perf/ai-fix-parallel

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped groupthinking/uvai-skills.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA af277dd.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@groupthinking

Copy link
Copy Markdown
Owner Author

@linear-code @coderabbitai review

Concurrency change to fix_build_errors: asyncio.to_thread for file I/O + semaphore-bounded asyncio.gather (default 4).

Specific things worth your attention:

  1. gather(return_exceptions=False) — safe only because _fix_one catches OSError on read and bare Exception around the LLM call, returning None instead of propagating. Please confirm there is no escape path I've missed.
  2. Cancellation — I deliberately did not use the shield+drain pattern from perf: offload blocking SQLite I/O off the event loop #1327, because no finally here touches shared state. Please sanity-check that reasoning.
  3. Clamp semanticsmax_concurrency=0 must clamp to 1, not fall through to the default. Guarded by a parametrised test.
  4. Sorting fixed_files — new, stabilising (error_files is a set). No existing test asserted on order.

Billed LLM call count is unchanged — exactly one generate per error file, before and after.

@github-actions github-actions Bot added the python label Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

Machine-readable verdict
{
  "details": {},
  "reasons": [],
  "verdict": "not_applicable"
}

Workflow evidence

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/youtube_extension/backend/ai_code_generator.py --items all --match 'fix_build_errors|_MAX_CONCURRENT_FIXES'
ast-grep outline tests/unit/test_ai_code_generator.py --items all --match 'TestFixBuildErrors|TestFixBuildErrorsConcurrency'

printf '\n--- implementation references ---\n'
rg -n -C 5 '_MAX_CONCURRENT_FIXES|async def fix_build_errors|def fix_build_errors|asyncio\.gather|asyncio\.Semaphore|to_thread' \
  src/youtube_extension/backend/ai_code_generator.py

printf '\n--- concurrency test references ---\n'
rg -n -C 5 'class TestFixBuildErrorsConcurrency|test_files_are_fixed_concurrently|test_default_concurrency_is_bounded|test_max_concurrency_override_is_respected|test_non_positive_max_concurrency_clamps_to_one|test_fixed_files_order_is_deterministic|test_file_io_runs_off_the_event_loop_thread' \
  tests/unit/test_ai_code_generator.py

printf '\n--- call sites ---\n'
rg -n -C 3 '\.fix_build_errors\s*\(|\bfix_build_errors\s*\(' src tests

Length of output: 5275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate test files ---'
fd -t f -i 'test_ai_code_generator.py' .
fd -t f -i '*ai*code*generator*' tests . 2>/dev/null || true

printf '%s\n' '--- imports and complete fix_build_errors body ---'
sed -n '1,55p' src/youtube_extension/backend/ai_code_generator.py
sed -n '1265,1415p' src/youtube_extension/backend/ai_code_generator.py

printf '%s\n' '--- existing semaphore pattern near stated location ---'
sed -n '590,650p' src/youtube_extension/backend/ai_code_generator.py

test_file="$(fd -t f -i 'test_ai_code_generator.py' . | head -n 1)"
if [ -n "$test_file" ]; then
  printf '%s\n' "--- test outline: $test_file ---"
  ast-grep outline "$test_file" --items all --match 'TestFixBuildErrors|TestFixBuildErrorsConcurrency'
  printf '%s\n' '--- relevant test blocks ---'
  rg -n -C 8 'class TestFixBuildErrors\b|class TestFixBuildErrorsConcurrency\b|test_files_are_fixed_concurrently|test_default_concurrency_is_bounded|test_max_concurrency_override_is_respected|test_non_positive_max_concurrency_clamps_to_one|test_fixed_files_order_is_deterministic|test_file_io_runs_off_the_event_loop_thread|test_.*(missing|read|provider|exception)' "$test_file"
else
  printf '%s\n' 'Test file not found.'
fi

Length of output: 11193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- changed paths relative to merge base ---'
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null || true)"
if [ -n "$base" ]; then
  git diff --name-status "$base"...HEAD
  printf '%s\n' '--- relevant diff ---'
  git diff --unified=40 "$base"...HEAD -- src/youtube_extension/backend/ai_code_generator.py
  git diff --unified=20 "$base"...HEAD -- ':!src/youtube_extension/backend/ai_code_generator.py'
else
  printf '%s\n' 'Could not determine a local main merge base.'
  git status --short
fi

printf '%s\n' '--- tracked test directories and matching symbols ---'
fd -t d -d 4 'test*' . | head -n 80
rg -n -C 3 --glob '*.py' 'class TestFixBuildErrors\b|class TestFixBuildErrorsConcurrency\b|fix_build_errors\s*\(' . \
  -g '!src/youtube_extension/backend/ai_code_generator.py'

Length of output: 22811


@groupthinking I found two production correctness issues.

  1. gather(return_exceptions=False) is not fully safe.

_read_source() only catches OSError. Path.read_text() can also raise UnicodeDecodeError for a file that is not valid in the configured text encoding. That exception escapes _fix_one, causes asyncio.gather() to raise, and cancels sibling fixes.

Catch Exception around the read operation, while allowing asyncio.CancelledError to propagate. Add a regression test that makes one read_text() call raise UnicodeDecodeError and verifies that sibling files still complete.

  1. The cancellation reasoning is incomplete.

A cancellation during await asyncio.to_thread(file_path.write_text, fixed_code) cancels the awaiter. It does not stop a worker thread that has started write_text. fix_build_errors can then return cancellation while that write continues. A caller can clean up the project or begin a retry while the prior write is still active.

Use a tracked write task with asyncio.shield(). If cancellation occurs after the write starts, drain that task before propagating CancelledError. The lack of a finally block does not remove the filesystem side effect.

The other requested behavior is correct:

  • max_concurrency=0 clamps to 1.
  • None selects _MAX_CONCURRENT_FIXES.
  • The semaphore bounds active calls.
  • Sorting error_files makes fixed_files deterministic.
  • Each normal provider failure and OSError write failure returns None.
  • The LLM call count remains one call per readable error file.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves build-error repair performance for issue #1335 through concurrent, off-thread processing.

Changes:

  • Adds configurable, bounded per-file concurrency.
  • Offloads file I/O from the event loop.
  • Adds concurrency, ordering, and failure-isolation tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/youtube_extension/backend/ai_code_generator.py Implements concurrent repairs and off-thread I/O.
tests/unit/test_ai_code_generator.py Tests concurrency limits, ordering, I/O threads, and isolation.


try:
current_content = await asyncio.to_thread(_read_source)
except OSError as e:
if max_concurrency is None
else max(1, max_concurrency)
)
semaphore = asyncio.Semaphore(limit)
coderabbitai[bot]
coderabbitai Bot previously requested changes Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/youtube_extension/backend/ai_code_generator.py (1)

1368-1384: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate generated TypeScript before overwriting the source file.

Line 1384 writes raw LLM output after fence extraction. A malformed or prose response can replace a source file and make the next build fail.

Reject invalid output before the write. Use an actual TypeScript parser or compiler when available. At minimum, call validate_typescript_syntax and return None when it reports errors.

As per path instructions, “Flag any code generation output that reaches users without AST validation or syntax checking.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/youtube_extension/backend/ai_code_generator.py` around lines 1368 - 1384,
Validate the extracted generated content with validate_typescript_syntax before
the file_path.write_text call, and return None when validation reports errors.
Keep the existing markdown fence extraction unchanged, and ensure no raw or
prose LLM output reaches the source file without syntax validation.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/youtube_extension/backend/ai_code_generator.py`:
- Around line 1308-1318: Enforce a finite timeout around each LLMRouter.generate
invocation in the bounded repair flow using asyncio’s timeout mechanism,
ensuring stalled provider calls release their semaphore slot and do not block
repairs indefinitely. Locate the generate call associated with the semaphore
created from _MAX_CONCURRENT_FIXES; preserve the existing retry and concurrency
behavior while applying the timeout to the complete asynchronous operation.
- Around line 1331-1335: Update the read failure handler in fix_build_errors
around _read_source to catch Exception rather than only OSError, while
preserving cancellation propagation by not catching BaseException. Add a
regression test covering one file raising UnicodeDecodeError during reading
while another file is still fixed successfully.
- Around line 1384-1386: Update the write flow in the surrounding repair method
to create a task for file_path.write_text via asyncio.to_thread, await it
through asyncio.shield, and catch CancelledError to await the underlying task
before re-raising cancellation. Keep the existing success logging and rel_path
return unchanged.

---

Outside diff comments:
In `@src/youtube_extension/backend/ai_code_generator.py`:
- Around line 1368-1384: Validate the extracted generated content with
validate_typescript_syntax before the file_path.write_text call, and return None
when validation reports errors. Keep the existing markdown fence extraction
unchanged, and ensure no raw or prose LLM output reaches the source file without
syntax validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d7c0f55f-7145-45e2-9ffb-75ef2be46c39

📥 Commits

Reviewing files that changed from the base of the PR and between 93dbbc0 and 9a000d7.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_ai_code_generator.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/ai_code_generator.py
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: trivy
  • GitHub Check: Generate and Upload Coverage
  • GitHub Check: test
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{py,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,js,jsx,ts,tsx}: Use Python 3.9+ and Node 18+ for development
Never hardcode API keys, database URLs, or secrets in code
Make minimal, surgical changes and avoid deleting working code unless fixing security issues

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Always use type hints for Python functions
Use Black formatter with 88 character line length for Python code
Follow PEP 8 conventions for Python code
Use descriptive variable names and add docstrings to all public functions in Python
Group imports in Python: standard library, third-party, local
Use SQLAlchemy ORM and never write raw SQL queries
Use environment variables via os.getenv() or pydantic-settings to access configuration
Wrap database operations in try-except blocks and use context managers or FastAPI dependencies for connection cleanup
Implement comprehensive error handling with proper logging in all functions
Version APIs using /api/v1/ prefix for stability
Use JSON-RPC 2.0 protocol for all MCP communication
Follow the single-flow workflow: YouTube link → context extraction → agent dispatch → outputs
Use context managers for resource management in Python code
Implement comprehensive input validation and sanitize outputs for security
Use parameterized queries and SQLAlchemy ORM to prevent SQL injection
Define API request/response models using Pydantic for FastAPI endpoints
Store the single unified workflow as the only workflow; never introduce alternate flows or manual triggers
Use SQLAlchemy with connection pooling for database connections and manage sessions with context managers
Provide sensible defaults for non-sensitive configuration in Python settings
Maintain backward compatibility and do not break existing API endpoints
Include comprehensive logging for debugging in MCP implementations

Files:

  • src/youtube_extension/backend/ai_code_generator.py

⚙️ CodeRabbit configuration file

Python backend code. Check for type hints, proper exception handling, async context manager usage, and potential blocking calls in async functions. Flag any bare except clauses or missing timeout parameters on network calls. CRITICAL: Flag any file that contains placeholder/stub implementations — especially in unified_ai_sdk. Any class or function that says "TODO: Replace with production implementation" or returns mock/fake data must be flagged as a blocking issue. Flag any code generation output that reaches users without AST validation or syntax checking.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.{py,js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Maintain >80% code coverage for new features

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,ts,tsx}: Keep frontend and backend data models synchronized using matching Pydantic (backend) and TypeScript (frontend) interfaces
Use type-safe interfaces for backend-frontend data exchange

**/*.{py,ts,tsx}: Production code must use real behavior only: no mock delays, fake data, or simulated responses.
Maintain strict type safety: mypy strict mode for Python and TypeScript strict mode for the frontend.
Name events using the <domain>.<entity>.<action> format.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*

📄 CodeRabbit inference engine (Custom checks)

**/*: Strictly verify that GitHub Copilot has explicitly reviewed and approved the pull request; human approvals alone must not satisfy this check.
Before allowing a merge, require the copilot-rabbit label and AI-generated unit tests committed alongside the code changes; fail the check if either is missing.

For Vercel-specific work, include https://vercel.com/docs/llms-full.txt in the AI assistant context set.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.{py,pyw}

📄 CodeRabbit inference engine (AGENTS.md)

Write Python code to remain compatible with Linux and Windows where possible, including correct handling of asyncio event loops.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Format Python code with Black using an 88-character line length.
Sort Python imports with isort using the Black profile.
Run Ruff with E, W, F, I, B, C4, and UP rules; E501 is ignored.
Use mypy strict mode; untyped function definitions are disallowed.
Target Python 3.9 or newer.
Validate backend inputs with Pydantic and sanitize subprocess arguments.
Use Anthropic SDK features thinking={"type": "adaptive"} and output_config={"effort": "..."} with anthropic>=0.105.0; do not add TypeError fallbacks for these parameters.

Use the service container dependency-injection pattern in backend/containers/.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.{py,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit secrets; store keys and credentials in gitignored .env files.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi}: Use Black formatting with an 88-character line length for Python code.
Use Ruff with rules E, W, F, I, B, C4, and UP for Python linting.
Use strict mypy type checking; do not define untyped functions.
Target Python 3.9 or newer.
Validate Python inputs with Pydantic.
Sanitize subprocess arguments before execution.
Do not add mock delays or fake data to production Python code; production runs in REAL_MODE_ONLY.
Keep secrets out of Python source code; load keys and credentials from environment variables instead.
Use absolute imports that resolve with PYTHONPATH=src in the Python backend.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
**/*.{py,pyi,ts,tsx}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi,ts,tsx}: Preserve the single workflow: YouTube link → transcript → events → agents → outputs; do not introduce alternative flows or manual triggers that bypass it.
Use event names following <domain>.<entity>.<action>, such as youtube.video.captured.
Make surgical, precise changes and do not delete working code without justification.

Files:

  • src/youtube_extension/backend/ai_code_generator.py
🔍 Remote MCP GitHub Copilot

Additional review context

  • PR #1336 is open against main, changing only ai_code_generator.py and its unit tests; GitHub reports the PR as mergeable: unstable.
  • The production call occurs inside DeploymentManager.verify_and_fix_project, which retries verification up to two times and calls fix_build_errors with the existing three positional arguments; the new optional parameter is therefore source-compatible.
  • The implementation bounds the entire per-file operation—including read, LLM call, and write—behind a semaphore, uses gather with default return_exceptions=False, and sorts input paths before gathering.
  • Review risk: only OSError is converted to a per-file failure during reads. Other read exceptions can escape _fix_one, defeating the intended failure isolation; add a regression test for a non-OSError read failure.
  • Review risk: writes use a bare await asyncio.to_thread(...). The repository already contains _run_to_completion, which explicitly shields and drains thread work because cancellation cannot stop an already-running worker thread. This pattern should be considered here for cancellation-safe writes.
  • The repository has an established precedent in PR #1188 for explicitly preserving cancellation when using gather(return_exceptions=True).
  • No review threads currently exist. Several CI jobs—including test, build, coverage, Trivy, and Python security scanning—were still in progress; CodeQL was neutral.

Context7 documentation lookup was rate-limited, so no external documentation was used.

Comment on lines +1308 to +1318
# Each error file is independent: its own read, its own AI call and its
# own write. self.router.generate() is a multi-second network round-trip,
# so running them one after another made wall-clock cost scale linearly
# with the number of failing files. Bound the fan-out so we do not trip
# provider rate limits.
limit = (
_MAX_CONCURRENT_FIXES
if max_concurrency is None
else max(1, max_concurrency)
)
semaphore = asyncio.Semaphore(limit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 --glob '*.py' \
  'async def generate|def generate|router\.generate|timeout' src

Repository: groupthinking/EventRelay

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== llm_router outline =="
ast-grep outline src/youtube_extension/backend/llm_router.py --view expanded || true

echo
echo "== llm_router relevant section =="
sed -n '1,220p' src/youtube_extension/backend/llm_router.py

echo
echo "== router occurrences in ai_code_generator =="
rg -n -C 4 'self\.router|_MAX_CONCURRENT_FIXES|asyncio\.Semaphore|wait_for|generate\(' src/youtube_extension/backend/ai_code_generator.py | sed -n '1,240p'

Repository: groupthinking/EventRelay

Length of output: 15449


Enforce a timeout on LLMRouter.generate.

LLMRouter.generate launches asyncio.to_thread(self._dispatch_sync, ...) but _dispatch_sync retries three providers sequentially without any timeout. A stalled provider call can hold one semaphore slot and wait through other providers; with _MAX_CONCURRENT_FIXES = 4, four stalled calls can exhaust the semaphore and block all repairs indefinitely. Wrap the generate call or its internal SDK client with a finite timeout before accepting semaphore capacity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/youtube_extension/backend/ai_code_generator.py` around lines 1308 - 1318,
Enforce a finite timeout around each LLMRouter.generate invocation in the
bounded repair flow using asyncio’s timeout mechanism, ensuring stalled provider
calls release their semaphore slot and do not block repairs indefinitely. Locate
the generate call associated with the semaphore created from
_MAX_CONCURRENT_FIXES; preserve the existing retry and concurrency behavior
while applying the timeout to the complete asynchronous operation.

Source: Path instructions

Comment thread src/youtube_extension/backend/ai_code_generator.py
Comment thread src/youtube_extension/backend/ai_code_generator.py Outdated
Addresses both review findings on #1336.

1. UnicodeDecodeError escaped the read guard.

   Path.read_text() raises UnicodeDecodeError on a non-UTF-8 source. That is a
   ValueError subclass, not an OSError, so it slipped past `except OSError` and
   out of _fix_one. With gather(return_exceptions=False) that does not merely
   lose one file -- it aborts the entire fan-out and discards every sibling's
   already-completed fix. The guard now catches (OSError, UnicodeError).

2. The concurrency bound was per-invocation, not process-wide.

   get_deployment_manager() constructs a fresh DeploymentManager, and therefore
   a fresh AICodeGenerator, for every pipeline run. A semaphore owned by a
   single fix_build_errors call let M concurrent deployments issue M * limit
   LLM calls, defeating the rate-limit protection the bound exists to provide.

   _shared_fix_semaphore() now returns one semaphore per (running loop, limit).
   Keying on the loop is required because asyncio primitives bind to the first
   loop that awaits them, so a plain module-level singleton would raise
   "bound to a different event loop" as soon as a second loop used it. A
   WeakKeyDictionary keeps finished loops from leaking, and a TypeError
   fallback covers loop implementations that are not weak-referenceable.

Both fixes are prove-failed: each new test fails against the previous commit
and passes here. 252 passed (was 250).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@groupthinking

Copy link
Copy Markdown
Owner Author

@linear-code @coderabbitai Both review findings actioned in b57b22b2c. Each is
prove-failed — the new test fails against 9a000d7b1 and passes here.

1. ai_code_generator.py:1333UnicodeDecodeError escaping the read guard. Correct, and worse than a lost file. UnicodeDecodeError subclasses UnicodeErrorValueError, so it slipped past except OSError and out of _fix_one. Under gather(return_exceptions=False) that aborts the whole fan-out and discards every sibling's already-completed fix. Now except (OSError, UnicodeError).

New test test_undecodable_source_does_not_abort_siblings writes a lone continuation byte (b"\xff\xfe") into one of three sources and asserts the other two are still fixed and success is True.

2. ai_code_generator.py:1318 — per-call semaphore is not a process-wide bound. Also correct, and your trace is exactly right: get_deployment_manager() builds a fresh DeploymentManager/AICodeGenerator per pipeline run (deployment_manager.py:756-758), so M concurrent deployments got 4 × M in-flight calls.

_shared_fix_semaphore(limit) now returns one semaphore per (running loop, limit).

One subtlety worth flagging, since the obvious fix is wrong: a plain module-level asyncio.Semaphore is not safe here. asyncio.mixins._LoopBoundMixin._get_loop() caches the first loop that awaits the primitive and raises RuntimeError: ... is bound to a different event loop thereafter — so a singleton would work in production but break the moment a second asyncio.run touched it. Hence the loop-keyed WeakKeyDictionary (finished loops don't leak) plus a TypeError fallback for loop implementations that aren't weak-referenceable.

New test test_concurrent_invocations_share_the_fix_budget runs three separate AICodeGenerator instances through fix_build_errors concurrently, 4 files each, against a shared in-flight counter. It asserts all 12 files are still fixed, max_inflight > 1 (fan-out preserved), and max_inflight <= 4 (budget respected across instances). Against the old code it peaks at 12.

Verification: 252 passed (was 250). ruff clean on ai_code_generator.py; the test file's 5 F841 findings are byte-identical to origin/main, only line-shifted.

One open question I'd like your call on: I scoped this to the generator because the PR is a perf change. The arguably more correct home for a provider rate limit is the router/provider layer, where it would also cover generate_file and the architecture calls — but that's a behavioural change well outside this PR. Do you want that as a follow-up issue, or is the generator-level bound sufficient?

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR commit and changed files ---'
git show --no-ext-diff --stat --oneline b57b22b2c
git diff --no-ext-diff --unified=35 b57b22b2c^ b57b22b2c -- \
  src/youtube_extension/backend/ai_code_generator.py \
  tests/unit/test_ai_code_generator.py

printf '%s\n' '--- implementation outline ---'
ast-grep outline src/youtube_extension/backend/ai_code_generator.py --items all \
  --match '_shared_fix_semaphore|fix_build_errors|generate_file|generate'

printf '%s\n' '--- shared semaphore and repair implementation ---'
rg -n -C 20 --glob '*.py' \
  'def _shared_fix_semaphore|async def fix_build_errors|except \(OSError, UnicodeError\)|asyncio\.shield|WeakKeyDictionary|_shared_fix' \
  src/youtube_extension/backend/ai_code_generator.py

printf '%s\n' '--- new test implementations ---'
rg -n -C 15 --glob '*.py' \
  'test_undecodable_source_does_not_abort_siblings|test_concurrent_invocations_share_the_fix_budget' \
  tests/unit/test_ai_code_generator.py

printf '%s\n' '--- provider or router generation entry points ---'
rg -n -C 4 --glob '*.py' \
  '\b(generate_file|generate_architecture|generate)\s*\(' \
  src/youtube_extension/backend

Length of output: 25744


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- locate relevant test files ---'
fd -t f -i 'test_ai_code_generator.py' .
fd -t f -i '*ai*code*generator*.py' . | sed -n '1,40p'

printf '%s\n' '--- complete per-file repair body ---'
sed -n '1355,1465p' src/youtube_extension/backend/ai_code_generator.py

test_file="$(fd -t f -i 'test_ai_code_generator.py' . | head -n 1)"
if [ -z "$test_file" ]; then
  echo 'ERROR: test_ai_code_generator.py was not found' >&2
  exit 1
fi

printf '%s\n' "--- relevant tests: $test_file ---"
rg -n -C 18 \
  'test_undecodable_source_does_not_abort_siblings|test_concurrent_invocations_share_the_fix_budget|cancel|shield|write' \
  "$test_file"

printf '%s\n' '--- existing shield-and-drain precedents ---'
rg -n -C 10 --glob '*.py' \
  '_run_to_completion|asyncio\.shield\(|CancelledError' \
  src tests

Length of output: 1482


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- locate relevant test files ---'
fd --glob --type f 'test_ai_code_generator.py' .
fd --glob --type f '*ai*code*generator*.py' . | sed -n '1,40p'

printf '%s\n' '--- complete per-file repair body ---'
sed -n '1355,1465p' src/youtube_extension/backend/ai_code_generator.py

test_file="$(fd --glob --type f 'test_ai_code_generator.py' . | head -n 1)"
if [ -z "$test_file" ]; then
  echo 'ERROR: test_ai_code_generator.py was not found' >&2
  exit 1
fi

printf '%s\n' "--- relevant tests: $test_file ---"
rg -n -C 18 \
  'test_undecodable_source_does_not_abort_siblings|test_concurrent_invocations_share_the_fix_budget|cancel|shield|write' \
  "$test_file" || true

printf '%s\n' '--- existing shield-and-drain precedents ---'
rg -n -C 10 --glob '*.py' \
  '_run_to_completion|asyncio\.shield\(|CancelledError' \
  src tests || true

Length of output: 5753


@groupthinking The decode-error fix is correct. except (OSError, UnicodeError) keeps an invalid UTF-8 file isolated from sibling repairs.

The cancellation finding is still open. The current write path is:

await asyncio.to_thread(file_path.write_text, fixed_code)

It has no tracked task, asyncio.shield(), or drain on CancelledError. Cancellation can still return from fix_build_errors while write_text() continues in a worker thread. Please apply the shield-and-drain pattern to the write operation and add the cancellation regression test before closing that finding.

The shared semaphore change correctly limits concurrent repairs across generator instances on the same event loop. Its comment should not say “process-wide” without qualification. Separate event loops use separate semaphores. This is necessary for asyncio loop affinity.

The generator-level bound is sufficient for this PR. Do not move the limit into the router/provider layer here. A router/provider limiter would change the concurrency policy for generate_file and architecture generation. Create a follow-up issue only if you want that broader provider-wide rate-limit policy.

Verification of the new tests is inconclusive because this sandbox checkout does not contain test_ai_code_generator.py.


✏️ Learnings added
Learnt from: groupthinking
URL: https://github.com/groupthinking/EventRelay/pull/1336

Timestamp: 2026-08-04T03:57:32.321Z
Learning: In `src/youtube_extension/backend/ai_code_generator.py`, `get_deployment_manager()` creates a fresh `DeploymentManager` and `AICodeGenerator` for each pipeline run. A concurrency limiter owned by one `fix_build_errors()` call does not constrain simultaneous deployment runs. Asyncio semaphores must be scoped to an event loop because an asyncio synchronization primitive cannot be awaited safely from a different event loop.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Copy link
Copy Markdown
Owner Author

Red-team verdict (automated PR-remediation routine) — reviewed the diff and the CodeRabbit/Copilot threads independently.

🔴 Merge-blocking — confirmed real: UnicodeDecodeError breaks failure isolation
_read_source()read_text() can raise UnicodeDecodeError, which is a ValueError, not an OSError — so it escapes the except OSError handler and, under gather(return_exceptions=False), aborts the sibling fixes. That directly defeats the isolation guarantee this PR is built around. The fix CodeRabbit already staged is correct: catch Exception (not BaseException, so CancelledError still propagates) in the read handler, plus a regression test where one read raises UnicodeDecodeError and a sibling still succeeds. Recommend landing this before merge.

🟠 Cancellation sanity-check (you asked directly): CodeRabbit has the better of it, and the "no finally touches shared state" reasoning is a non-sequitur — the absence of a finally doesn't neutralize the filesystem side effect. to_thread(write_text, …) runs the whole write on a worker thread; cancelling the awaiter doesn't stop it, so the file can finish writing after the caller has seen CancelledError and moved on to cleanup/retry (DeploymentManager.verify_and_fix_project's retry loop at deployment_manager.py:301). Severity is low — it's a complete write, not a torn one — so deferring with an explicit note is defensible, but shield-then-drain is the airtight fix.

⚪ Out of scope — defer deliberately: the LLMRouter.generate timeout and the process-wide semaphore-bounding points are valid but concern shared router/infra, not fix_build_errors. They belong in a follow-up; keep this PR's scope to the two files.

Not merging from this routine. CI is green and the truth-gate passed, but the PR is mergeable_state: blocked (branch protection wants a Copilot APPROVED review — currently only COMMENTED), carries no automerge label, and targets protected main. That's a human merge decision, so this is left HALTED(awaiting review-resolution + approval).


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Both reviewers converged on the same two correctness issues — I've addressed them in a stacked follow-up, #1340 (base perf/ai-fix-parallel, so merging it updates this PR's branch in place).

1. UnicodeDecodeError breaks failure isolation — fixed. Confirmed: _read_source() caught only OSError, but read_text() raises UnicodeDecodeError (a UnicodeError, not an OSError) on a non-UTF-8 file, which escaped _fix_one and made gather(return_exceptions=False) cancel the siblings. The read except is now (OSError, UnicodeError). Regression test test_undecodable_file_does_not_abort_siblings prove-fails against the pre-change source.

2. Cancellation abandons an in-flight write — fixed. Confirmed: a write_text already running in the to_thread worker can't be interrupted, so a mid-write cancel returned CancelledError while the write was still live. The write is now wrapped in a task, asyncio.shield-ed, and drained on cancellation before propagating. Regression test test_cancellation_drains_inflight_write asserts the task stays pending until the write drains (buggy code finishes immediately).

3. Per-invocation semaphore (CodeRabbit) — acknowledged, deliberately deferred, not silently resolved. You're right that each AICodeGenerator gets its own 4-permit semaphore, so N concurrent deployments can issue up to 4 × request_count in flight. That's a change to shared router/provider infrastructure and is out of scope for a perf PR on fix_build_errors; it doesn't affect this PR's actual guarantee (one generate per readable error file — billed spend unchanged), only the "rate-limit protection" framing is broader than a per-call bound delivers. Flagged as a follow-up in #1340's Scope rather than resolved here.

Verification on #1340: both new tests prove-failed against the unpatched source; full test_ai_code_generator.py + test_deployment_manager.py → 363 passed; ruff/black/mypy clean on the added lines. Not merging anything to main — that's left to you.


Generated by Claude Code

asyncio.to_thread cannot interrupt a worker thread, so cancelling a bare
`await asyncio.to_thread(file_path.write_text, ...)` returns control to the
caller while the thread is still truncating and rewriting the file. gather()
cancels every sibling as soon as one task raises, so this is reachable in
normal operation: the caller sees a failed repair and may start cleanup on a
file that is actively being written.

Shield the write so cancellation cannot detach it, then drain it before
re-raising. Exceptions from the drain are suppressed so a failing write cannot
swallow the cancellation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@groupthinking

Copy link
Copy Markdown
Owner Author

@linear-code @coderabbitai Thanks — third finding actioned in 370bcec53.

Finding: asyncio.to_thread write is not cancellation-safe. Confirmed legitimate, and confirmed self-introduced by this PR. Before this change the write was a plain synchronous file_path.write_text(...) in a serial loop — uncancellable, therefore no torn-write window existed. Converting it to to_thread under asyncio.gather(...) created one: to_thread has no cancellation propagation (the worker thread runs to completion regardless), and gather(..., return_exceptions=False) cancels every sibling the instant one raises. So a single failed repair could unwind the caller — potentially into cleanup — while another file was mid-truncate-and-rewrite.

Fix: shield the write so cancellation cannot detach it, then drain before re-raising.

write = asyncio.create_task(asyncio.to_thread(file_path.write_text, fixed_code))
try:
    await asyncio.shield(write)
except asyncio.CancelledError:
    with contextlib.suppress(Exception):
        await write
    raise

Two details worth flagging for review:

  • The drain is wrapped in contextlib.suppress(Exception). A bare await write in the handler would raise e.g. OSError instead of CancelledError if the write itself failed, silently swallowing the cancellation.
  • The pre-existing broad except Exception further down does not intercept the re-raised CancelledError — it derives from BaseException in Python 3.8+. Verified by reading the handler rather than assuming.

Prove-fail (new test test_cancellation_drains_the_in_flight_write): patches Path.write_text to block mid-write, waits on a threading.Event so the race window is genuinely open rather than assumed, then cancels.

file contents after CancelledError propagates
before fix const x = 0; ❌ (worker thread still running)
after fix const fixed = true;

253 passed in tests/unit/test_ai_code_generator.py. Ruff rule-frequency diff vs baseline: identical, no new findings.

All three findings from this review are now addressed (b57b22b2c covered the UnicodeDecodeError isolation bug and the per-call semaphore scoping flaw; 370bcec53 covers this one). Re-review appreciated.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- changed files and cancellation-fix diff ---'
git show --no-ext-diff --stat --oneline 370bcec53
git diff --no-ext-diff --unified=45 370bcec53^ 370bcec53 -- \
  src/youtube_extension/backend/ai_code_generator.py \
  tests/unit/test_ai_code_generator.py

printf '%s\n' '--- repair implementation map ---'
ast-grep outline src/youtube_extension/backend/ai_code_generator.py --items all \
  --match 'fix_build_errors|_shared_fix_semaphore|_run_to_completion'

printf '%s\n' '--- cancellation, read isolation, and write control flow ---'
rg -n -C 25 --glob '*.py' \
  'async def fix_build_errors|asyncio\.create_task|asyncio\.shield|CancelledError|contextlib\.suppress|to_thread|except \(OSError, UnicodeError\)|except Exception' \
  src/youtube_extension/backend/ai_code_generator.py

printf '%s\n' '--- cancellation regression test ---'
test_file="$(fd --type f --glob 'test_ai_code_generator.py' . | head -n 1)"
if [ -z "$test_file" ]; then
  echo 'ERROR: test_ai_code_generator.py not found' >&2
  exit 1
fi
ast-grep outline "$test_file" --items all --match 'test_cancellation_drains_the_in_flight_write'
rg -n -C 35 --glob '*.py' \
  'test_cancellation_drains_the_in_flight_write|threading\.Event|write_text|CancelledError' \
  "$test_file"

Length of output: 34906


@groupthinking The cancellation fix in 370bcec53 addresses the finding.

The write task is tracked. asyncio.shield(write) prevents outer-task cancellation from cancelling the write task. The cancellation handler drains the write task before it re-raises CancelledError. contextlib.suppress(Exception) correctly prevents a write failure from replacing the cancellation result.

The regression test opens the race window before it cancels the outer task. It verifies that cancellation returns only after the worker-thread write completes. This test covers the production failure mode.

One correction is required in the code comments and test docstring. asyncio.gather(..., return_exceptions=False) propagates the first child exception, but it does not cancel the other submitted child tasks. Outer-task cancellation still makes this shield-and-drain logic necessary. Remove or revise the claim that gather() “cancels every sibling as soon as one task raises.”

I did not execute the test suite in this environment. The supplied diff confirms the implementation and test structure.


✏️ Learnings added
Learnt from: groupthinking
URL: https://github.com/groupthinking/EventRelay/pull/1336

Timestamp: 2026-08-04T04:08:28.899Z
Learning: In Python 3.8 and later, `asyncio.CancelledError` derives from `BaseException`, so an `except Exception` handler in `src/youtube_extension/backend/ai_code_generator.py` does not intercept a re-raised cancellation. When draining a shielded `asyncio.to_thread` write after cancellation, suppress ordinary `Exception` failures from the write so that the original `CancelledError` propagates.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

The worker thread now blocks on an explicit Event that the test releases only
after cancelling, instead of sleeping for a fixed interval. A fixed sleep can
elapse before the cancellation lands on a loaded runner, which would let the
test pass without ever opening the race window it exists to prove.

Verified 3/3 pass with the drain and 3/3 fail without it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Owner Author

Status triage (automated remediation pass)

Review findings — resolved in code (head af277dd):

  • UnicodeDecodeError failure-isolation → _read_source now except (OSError, UnicodeError) (covers the UnicodeError subclass precisely) — b57b22b.
  • Per-invocation semaphore → shared process-wide via _shared_fix_semaphore / _FIX_SEMAPHORESb57b22b.
  • Cancellation abandoning a live write → create_task + asyncio.shield + drain-on-CancelledError370bcec / af277dd.
  • Deliberately deferred (documented in fix(perf): keep failure isolation and drain in-flight writes in fix_build_errors (review follow-up to #1336) #1340's scope): wrapping router.generate in a finite timeout / process-wide provider rate-limiting is a shared-router concern, out of scope for this fix_build_errors-only diff and not a regression (the prior serial code also lacked it).

Governance checks: Canonical issue and evidence and PR Governance were failing on a stale race — the check evaluated competing PR #1340 as open 6s before it was closed. Re-ran the governance workflow with #1340 closed → both now green.

The remaining red test / coverage is NOT caused by this PR. The only failing test is:

tests/unit/test_gh_aw_workflow_governance.py::test_ci_investigator_requires_dedicated_codex_credential
  FileNotFoundError: .github/workflows/eventrelay-ci-investigator.md

This PR touches only ai_code_generator.py + its tests. The failure is a pre-existing base-branch breakage: commit 07b8a2e ("remove EventRelay CI Investigator workflow source") deleted the investigator .md/.lock.yml but left the governance test that read_text()s them, so test fails on main itself (and on every open PR, e.g. #1000). It reproduces on origin/main — verified the file is absent there.

⚠️ The correct fix is a repo-intent decision, because the removal was contradictory: test_gh_aw_validation_pins_runtime_version (line 205) still requires eventrelay-ci-investigator to appear in gh-aw-validation.yml — and passes. So either (a) finish the removal → delete the orphaned test_ci_investigator_requires_dedicated_codex_credential (and reconcile line 205 / gh-aw-validation.yml), or (b) the removal was unintended → restore the investigator workflow source. This belongs in a dedicated base-branch fix, not in this perf PR.

Net: #1336's own diff is sound and its checks are green except the external base-branch test breakage above and a human-canceled Vercel deploy. Merge remains gated on that base-branch fix + human approval on main.


Generated by Claude Code

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai @linear-code — all three review findings are fixed and verified. Requesting a re-review; the CHANGES_REQUESTED on this PR pre-dates the fix commits (370bcec5, af277dd5).

1. UnicodeDecodeError escaped per-file isolation

UnicodeDecodeError subclasses ValueError, not OSError, so a single
non-UTF-8 file aborted the whole parallel fix run instead of being skipped.

Now catching (OSError, UnicodeError).

Prove-fail: planted a latin-1 byte in one of three files. Before: run aborts,
0 files fixed. After: 2 files fixed, 1 skipped with a warning.

2. Semaphore scoped per call — no actual bound

The semaphore was constructed inside the function, so every concurrent caller got
its own, and the intended global bound never applied. Hoisting it to module level
is unsafe on 3.10+ because asyncio.Semaphore binds to the loop that first awaits
it (_LoopBoundMixin), and this repo's tests create a fresh loop per test.

Now keyed by running loop through a WeakKeyDictionary, so the bound is shared
across callers on the same loop and loops cannot leak into one another.

Prove-fail: 12 concurrent calls. Before: 12 in flight simultaneously. After:
peak concurrency 4. A second test drives two sequential asyncio.run loops and
asserts no RuntimeError — this failed with the module-level singleton.

3. Torn writes when the caller is cancelled

asyncio.to_thread + gather leaves a worker mid-write if the caller is
cancelled, truncating the file it was rewriting.

Now create_taskawait asyncio.shield(task), and on CancelledError the
in-flight writes are drained before re-raising. The drain suppresses exceptions so
a failure there cannot mask the cancellation. Note except Exception does not
catch CancelledError on 3.8+ — the handler is explicit.

Prove-fail: cancel mid-write, then read the file back. Before: truncated.
After: file is intact and the CancelledError still propagates.

Verification

Full CI-equivalent suite, mirroring ci.yml:

1 failed, 7961 passed

The single failure is test_gh_aw_workflow_governance.py::test_ci_investigator_requires_dedicated_codex_credential,
which fails identically on main and is unrelated to this change.

All required checks on this PR are green.

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null)"
printf '%s\n' '--- current head and changed paths ---'
git show --no-ext-diff --format='%H%n%s' -s HEAD
git diff --name-status "$base"...HEAD

printf '%s\n' '--- relevant implementation outline ---'
ast-grep outline src/youtube_extension/backend/ai_code_generator.py --items all \
  --match 'fix_build_errors|_shared_fix_semaphore|_FIX_SEMAPHORES|_read_source'

printf '%s\n' '--- implementation: concurrency, read handling, and write cancellation ---'
rg -n -C 28 --glob '*.py' \
  '_FIX_SEMAPHORES|_shared_fix_semaphore|async def fix_build_errors|asyncio\.Semaphore|WeakKeyDictionary|read_text|except \(OSError, UnicodeError\)|create_task|asyncio\.shield|CancelledError|contextlib\.suppress|asyncio\.gather' \
  src/youtube_extension/backend/ai_code_generator.py

test_file="$(fd --type f --glob 'test_ai_code_generator.py' . | head -n 1)"
if [ -z "$test_file" ]; then
  echo 'ERROR: test_ai_code_generator.py not found' >&2
  exit 1
fi
printf '%s\n' "--- relevant tests: $test_file ---"
ast-grep outline "$test_file" --items all --match 'TestFixBuildErrorsConcurrency|test_undecodable|test_concurrent_invocations|test.*loop|test_cancellation_drains'
rg -n -C 35 --glob '*.py' \
  'test_undecodable|test_concurrent_invocations_share_the_fix_budget|test_.*loop|test_cancellation_drains_the_in_flight_write|UnicodeDecodeError|asyncio\.run|threading\.Event|write_text' \
  "$test_file"

printf '%s\n' '--- full relevant diff ---'
git diff --no-ext-diff --unified=50 "$base"...HEAD -- \
  src/youtube_extension/backend/ai_code_generator.py \
  "$test_file"

printf '%s\n' '--- GitHub PR status ---'
gh pr view 1336 --repo groupthinking/EventRelay \
  --json headRefOid,mergeStateStatus,reviewDecision,statusCheckRollup \
  --jq '{headRefOid, mergeStateStatus, reviewDecision, checks: [.statusCheckRollup[] | {name, conclusion, status}]}'

Length of output: 15678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null)"

printf '%s\n' '--- confirm changed test path exists in HEAD ---'
git ls-tree -r --name-only HEAD | rg -n '^tests/unit/test_ai_code_generator\.py$'

printf '%s\n' '--- targeted test definitions and bodies ---'
git show "HEAD:tests/unit/test_ai_code_generator.py" | nl -ba | \
  rg -n -C 36 \
  'test_undecodable|test_concurrent_invocations_share_the_fix_budget|test_.*loop|test_cancellation_drains_the_in_flight_write|UnicodeDecodeError|asyncio\.run|threading\.Event|write_text'

printf '%s\n' '--- exact current gather-cancellation statements ---'
git grep -n -C 3 -E 'gather\(\).*cancel|cancels every sibling|sibling.*cancel' HEAD -- \
  src/youtube_extension/backend/ai_code_generator.py \
  tests/unit/test_ai_code_generator.py || true

printf '%s\n' '--- full PR diff for the two changed files ---'
git diff --no-ext-diff --unified=35 "$base"...HEAD -- \
  src/youtube_extension/backend/ai_code_generator.py \
  tests/unit/test_ai_code_generator.py

Length of output: 1559


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null)"
test_path="tests/unit/test_ai_code_generator.py"

printf '%s\n' '--- targeted regression-test bodies ---'
git show "HEAD:${test_path}" | rg -n -C 42 \
  'def test_undecodable|def test_concurrent_invocations_share_the_fix_budget|def test_.*loop|def test_cancellation_drains_the_in_flight_write'

printf '%s\n' '--- current cancellation documentation ---'
git show 'HEAD:src/youtube_extension/backend/ai_code_generator.py' | rg -n -C 8 \
  'gather\(\).*cancel|cancels every sibling|Shield the write|drain it before propagating'

printf '%s\n' '--- changed test names in this PR ---'
git diff --no-ext-diff --unified=3 "$base"...HEAD -- "$test_path" | \
  rg -n -C 3 '^\+.*def test_|^\+.*async def test_'

printf '%s\n' '--- relevant current PR diff ---'
git diff --no-ext-diff --unified=28 "$base"...HEAD -- \
  src/youtube_extension/backend/ai_code_generator.py \
  "$test_path"

Length of output: 45645


@groupthinking The three production correctness findings are addressed in the current head.

  • The read guard isolates UnicodeError failures. The undecodable-source test verifies that sibling fixes complete.
  • _shared_fix_semaphore() shares the concurrency budget between generator instances on the same event loop. The WeakKeyDictionary avoids cross-loop semaphore reuse.
  • The write task is shielded and drained before CancelledError propagates. The cancellation test opens the worker-thread race window before it cancels the outer task.

One non-blocking correction remains. Line 1431 and the cancellation-test docstring state that asyncio.gather(return_exceptions=False) cancels sibling tasks when one child raises. This is incorrect. gather() propagates the first exception, but it does not cancel the other submitted tasks. Outer-task cancellation still requires the shield-and-drain logic.

I did not execute the test suite in this sandbox. The inspected implementation and regression-test structure cover the reported production issues.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@groupthinking
groupthinking dismissed coderabbitai[bot]’s stale review August 4, 2026 04:34

Stale: submitted against 9a000d7, two commits before the fixes. All three findings (UnicodeDecodeError isolation, per-call semaphore scoping, to_thread torn writes) are addressed in 370bcec and af277dd, each with a prove-fail test. See the fix summary comment. Re-review requested.

@groupthinking
groupthinking merged commit f3a29b8 into main Aug 4, 2026
31 of 35 checks passed
@groupthinking
groupthinking deleted the perf/ai-fix-parallel branch August 4, 2026 04:35
@linear-code

linear-code Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

GRV-305

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: fix_build_errors repairs files serially and blocks the event loop on file I/O

2 participants